package com.uobgroup.ews.uam.core.interceptor;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.log4j.Logger;
import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

import com.google.gson.Gson;
import com.uobgroup.ews.uam.core.context.ApplicationContextProvider;
import com.uobgroup.ews.uam.core.domain.BaseEntity;
import com.uobgroup.ews.uam.core.domain.ChangeLog;
import com.uobgroup.ews.uam.core.domain.SecurityGroupAware;
import com.uobgroup.ews.uam.core.enums.OperateType;
import com.uobgroup.ews.uam.core.service.ChangeLogService;
import com.uobgroup.ews.uam.core.service.ChangeLogServiceImpl;
import com.uobgroup.ews.uam.core.service.ldap.LDSService;
import com.uobgroup.ews.uam.core.service.ldap.LDSServiceImpl;

/**
 * Interceptor class for logging any operations on the entities of the
 * application
 * 
 * @author ndduy
 * 
 */
public class AuditInterceptor extends EmptyInterceptor {

   /**
    * 
    */
   private static final long serialVersionUID = 1L;
   
   private static Logger logger = Logger.getLogger(AuditInterceptor.class);
   
   public static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
   public static String NEWLINE = System.getProperty("line.separator");

   @Override
   public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
      if (entity instanceof BaseEntity) {
         createChangeLog((BaseEntity) entity, OperateType.INSERT);
      }
      if(entity instanceof SecurityGroupAware){
         createSecurityGroup((SecurityGroupAware)entity);
      }
      return false;
   }

   @Override
   public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
      if (entity instanceof BaseEntity) {
         createChangeLog((BaseEntity) entity, OperateType.DELETE);
      }
   }

   @Override
   public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState,
         String[] propertyNames, Type[] types) {
      if (entity instanceof BaseEntity) {
         createChangeLog((BaseEntity) entity, OperateType.UPDATE);
      }
      return false;
   }

   /**
    * Get the pre-updated object in the database for getting the changes
    * @author ndduy
    * @param entity {@link BaseEntity}
    * @return {@link Object}
    */
   private Object getOriginalObject(BaseEntity entity) {
      LocalContainerEntityManagerFactoryBean emf = (LocalContainerEntityManagerFactoryBean) ApplicationContextProvider
            .getApplicationContext().getBean(LocalContainerEntityManagerFactoryBean.class);
      EntityManager em = emf.nativeEntityManagerFactory.createEntityManager();
      return em.find(entity.getClass(), entity.getId());
   }

   /**
    * Add the new change log to the database
    * 
    * @author ndduy
    * @param entity
    *           {{@link BaseEntity}
    * @param operateType
    *           {@link OperateType}
    */
   private void createChangeLog(BaseEntity entity, OperateType operateType) {
      ChangeLogService changeLogService = (ChangeLogService) ApplicationContextProvider.getApplicationContext()
            .getBean(ChangeLogServiceImpl.class);
      
      ChangeLog changeLog = new ChangeLog();
      changeLog.setEntity(entity);
      changeLog.setEntityId(String.valueOf(entity.getId()));
      changeLog.setOperateTime(Calendar.getInstance().getTime());
      changeLog.setOperateType(operateType);
      if(entity.getClass().getAnnotation(Table.class) != null){
         changeLog.setTableName(entity.getClass().getAnnotation(Table.class).name());
      }else{
         changeLog.setTableName(entity.getClass().getSimpleName().toUpperCase());
      }
      
      changeLog.setUserId(entity.getLastModificationUserId());
      
      if (!operateType.equals(OperateType.UPDATE)) {
         BaseEntity changedObject = (BaseEntity)clone(entity);
         changedObject.setLastModificationUserId(null);
         changedObject.setLastModificationDate(null);
         changeLog.setChanges(new Gson().toJson(changedObject));
      }
      else {
         Object originalEntity = getOriginalObject(entity);
         Object newEntity = entity;

         //iterate through each field of the entity to get the differences
         Class<?> objectClass = newEntity.getClass();
         Field[] fields = objectClass.getDeclaredFields();
         fieldIteration: for (Field field : fields) {
            field.setAccessible(true);
            String fieldName = field.getName();
            if (!fieldName.equalsIgnoreCase("id") && !fieldName.equalsIgnoreCase("version")
                  && !fieldName.equalsIgnoreCase("hashcodevalue")) {
               //ignore static and transient fields
               if((field.getModifiers() & Modifier.STATIC) != 0
                     || field.getAnnotation(Transient.class) != null){
                  continue fieldIteration;
               }
               
               //Compare two list object based on the id
               Class<?> interfaces[] = field.getType().getInterfaces();
               for(int i = 0; i < interfaces.length; i++){
                  if(interfaces[i].getName().equalsIgnoreCase("java.util.Collection")){
                     try{
                        Object clonedOriginalEntity = clone(originalEntity);
                        Object clonedNewEntity = clone(newEntity);
                        if(field.getType().equals(List.class)){
                           List<BaseEntity> originFieldList = (List<BaseEntity>)field.get(clonedOriginalEntity);
                           List<BaseEntity> newFieldList = (List<BaseEntity>)field.get(clonedNewEntity);
                           if(originFieldList == null && newFieldList == null){
                              continue fieldIteration;
                           } else if((originFieldList != null && newFieldList == null)
                                 || (originFieldList == null && newFieldList != null)){
                              StringBuilder changes = new StringBuilder();
                              changes.append(field.getName()).append(": ");
                              changes.append((new Gson()).toJson(originFieldList));
                              changes.append(" -> ");
                              changes.append((new Gson()).toJson(newFieldList));
                              changes.append(NEWLINE);
                              changeLog.setChanges((changeLog.getChanges() == null ? "" : changeLog.getChanges()) + changes);
                           } else{
                              if(originFieldList.size() != newFieldList.size()){
                                 StringBuilder changes = new StringBuilder();
                                 changes.append(field.getName()).append(": ");
                                 changes.append((new Gson()).toJson(originFieldList));
                                 changes.append(" -> ");
                                 changes.append((new Gson()).toJson(newFieldList));
                                 changes.append(NEWLINE);
                                 changeLog.setChanges((changeLog.getChanges() == null ? "" : changeLog.getChanges()) + changes);
                              } else{
                                 Collections.sort(originFieldList, new BaseEntityComparator());
                                 Collections.sort(newFieldList, new BaseEntityComparator());
                                 for(int j = 0; j < originFieldList.size(); j++){
                                    if(!originFieldList.get(j).getId().equals(newFieldList.get(j).getId())){
                                       StringBuilder changes = new StringBuilder();
                                       changes.append(field.getName()).append(": ");
                                       changes.append((new Gson()).toJson(originFieldList));
                                       changes.append(" -> ");
                                       changes.append((new Gson()).toJson(newFieldList));
                                       changes.append(NEWLINE);
                                       changeLog.setChanges((changeLog.getChanges() == null ? "" : changeLog.getChanges()) + changes);
                                       break;
                                    }
                                 }
                              }
                           }
                        }
                     } catch (IllegalAccessException e) {
                        logger.error(e.getMessage());
                     } catch (ClassCastException e){
                        logger.error(e.getMessage());
                     }
                     continue fieldIteration;
                  }
               }
               
               try {
                  Object newValue = field.get(newEntity);
                  Object oldValue = field.get(originalEntity);
                  if(newValue == null && oldValue == null){
                     continue;
                  }
                  newValue = getEntityIdOrStringValue(newValue);
                  oldValue = getEntityIdOrStringValue(oldValue);
                  if(newValue != null && newValue.equals(oldValue)){
                     continue;
                  }
                  StringBuilder changes = new StringBuilder();
                  changes.append(fieldName).append(": ");
                  changes.append(oldValue == null ? "" : oldValue.toString());
                  changes.append(" -> ");
                  changes.append(newValue == null ? "" : newValue.toString());
                  changes.append(NEWLINE);
                  changeLog.setChanges((changeLog.getChanges() == null ? "" : changeLog.getChanges()) + changes);
               }
               catch (IllegalArgumentException e) {
                  logger.error(e.getMessage());
               }
               catch (IllegalAccessException e) {
                  logger.error(e.getMessage());
               }
            }
         }
      }
      if(changeLog.getChanges() != null && changeLog.getChanges().trim().length() > 0){
         changeLogService.addNew(changeLog);
      }
   }
   
   class BaseEntityComparator implements Comparator<BaseEntity>{

      @Override
      public int compare(BaseEntity o1, BaseEntity o2) {
         Long id1 = o1.getId() == null ? 0 : o1.getId();
         Long id2 = o2.getId() == null ? 0 : o2.getId();
         return id1.compareTo(id2);
      }
      
   }

   private String getEntityIdOrStringValue(Object obj) {
      if (obj == null)
         return null;

      if (!(obj instanceof BaseEntity)) {
         if (obj instanceof Date) {
            return DATE_FORMAT.format((Date) obj);
         }
         return obj.toString();
      }

      Long id = ((BaseEntity)obj).getId();
      return id == null ? "" : id.toString();
   }
   
   /**
    * Clone an object (just get the id of the nested classes)
    * @author ndduy
    * @param o
    * @return
    */
   private Object clone(Object o) {
      Object clone = null;

      try {
         clone = o.getClass().newInstance();
      }
      catch (InstantiationException e) {
         e.printStackTrace();
      }
      catch (IllegalAccessException e) {
         e.printStackTrace();
      }

      // Walk up the superclass hierarchy
      for (Class<?> obj = o.getClass(); !obj.equals(Object.class); obj = obj.getSuperclass()) {
         Field[] fields = obj.getDeclaredFields();
         for (int i = 0; i < fields.length; i++) {
            fields[i].setAccessible(true);
            if((fields[i].getModifiers() & Modifier.STATIC) != 0
                  || fields[i].getAnnotation(Transient.class) != null){
               continue;
            }
            try {
               // for each class/superclass, copy all fields
               // from this object to the clone
               if(fields[i].get(o) != null){
                  
                  /**
                   * if current field is another entity, just clone its id
                   */
                  Object referenceObject = null;
                  for(Field currentObjectField : fields[i].getType().getDeclaredFields()){
                     currentObjectField.setAccessible(true);
                     if(currentObjectField.getName().equals("id")){
                        referenceObject = fields[i].getType().newInstance();
                        for(Field newObjectField : referenceObject.getClass().getDeclaredFields()){
                           newObjectField.setAccessible(true);
                           if(newObjectField.getName().equals("id")){
                              newObjectField.set(referenceObject, currentObjectField.get(fields[i].get(o)));
                              break;
                           }
                        }
                        break;
                     }
                  }
                  
                  /**
                   * if current field is a collection, return a collection of items' id
                   */
                  List<Object> objectList = null;
                  boolean isCollections = false;
                  if(fields[i].getType().getInterfaces() != null
                        && fields[i].getType().getInterfaces().length > 0){
                     Class<?> interfaces[] = fields[i].getType().getInterfaces();
                     for(int j = 0; j < interfaces.length; j++){
                        if(interfaces[j].getName().equalsIgnoreCase("java.util.Collection")){
                           isCollections = true;
                           break;
                        }
                     }
                  }
                  if(isCollections){
                     if(fields[i].getType().equals(List.class)){
                        List<Object> listObj = (List<Object>)fields[i].get(o);
                        if(listObj != null && listObj.size() > 0){
                           objectList = new ArrayList<Object>();
                           for(Object ob : listObj){
                              Object newListItem = ob.getClass().newInstance();
                              for(Field field : ob.getClass().getDeclaredFields()){
                                 field.setAccessible(true);
                                 if(field.getName().equals("id")){
                                    for(Field newField : newListItem.getClass().getDeclaredFields()){
                                       newField.setAccessible(true);
                                       if(newField.getName().equals("id")){
                                          newField.set(newListItem, field.get(ob));
                                          objectList.add(newListItem);
                                          break;
                                       }
                                    }
                                    break;
                                 }
                              }
                           }
                        }
                     }
                  }
                  
                  if(referenceObject != null){
                     fields[i].set(clone, referenceObject);
                  } else if(objectList != null){
                     if(objectList.size() > 0){
                        fields[i].set(clone, objectList);
                     }
                  } else{
                     fields[i].set(clone, fields[i].get(o));
                  }
               }
            }
            catch (IllegalArgumentException e) {
               logger.error(e.getMessage());
            }
            catch (IllegalAccessException e) {
               logger.error(e.getMessage());
            }
            catch (InstantiationException e) {
               logger.error(e.getMessage());
            }
            catch (SecurityException e) {
               logger.error(e.getMessage());
            }
         }
      }
      return clone;
   }
   
   private void createSecurityGroup(SecurityGroupAware entity){
      LDSService ldsService = ApplicationContextProvider.getApplicationContext().getBean(
            LDSServiceImpl.class);
      if(entity.getSecurityGroup() != null){
         ldsService.createGroup(entity.getSecurityGroup());
      }
   }
}